/*
 * Copyright 2013 Sebastian Gesemann <s.gesemann@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * This is the LADSPA plugin's main source file
 */

#include <ladspa.h>

#include <cmath>
#include <algorithm>
#include <vector>
#include <exception>

#include "fft.hpp"

namespace sg {
namespace {

// This include file defines
// hrir512_length: length of the impulse responses in # of samples
// hrir512_gain:   gain factor
// hrir512_mid:    impulse response for L->L and R->R
// hrir512_sid:    impulse response for L->R and R->L (deliberate cross talk)
#include "table_hrir512.inc"
const int unique_id_hrir512 = 55;

// This include file defines
// hrir80_length: length of the impulse responses in # of samples
// hrir80_gain:   gain factor
// hrir80_mid:    impulse response for L->L and R->R
// hrir80_sid:    impulse response for L->R and R->L (deliberate cross talk)
#include "table_hrir80.inc"
const int unique_id_hrir80 = 56;

/// returns the smallest power of two that is equal or larger
/// than the input
unsigned nextpow2(unsigned n)
{
	unsigned ret = 1;
	while (ret<n) ret *= 2;
	return ret;
}

const unsigned num_ports = 4;

struct plugin_state
{
	LADSPA_Data* port_data_locations[num_ports];
	LADSPA_Data run_adding_gain;
	unsigned ir_length, fftsize;
	std::vector<complx> transfer; // FFT buffer for the impulse response
	std::vector<complx> input;    // FFT buffer for the input block
	std::vector<complx> output;   // FFT buffer for the filtered input
	std::vector<complx> save;     // mixing & save buffer
	bool cross_talk; // can be set to false for a speed-up if ir_sid is all zero

	plugin_state(
		unsigned ir_length, float gain,
		const float ir_mid[], const float ir_sid[]);

	void connect_port(unsigned long Port, LADSPA_Data * DataLocation)
	{ if (Port<num_ports) port_data_locations[Port]=DataLocation; }

	void set_run_adding_gain(LADSPA_Data rag)
	{ run_adding_gain = rag; }

	void reset_internal_state()
	{ save.clear(); }

	void activate()
	{ reset_internal_state(); }

	void run(unsigned long SampleCount, bool add = false);

	void deactivate() {}
};

static void copy_samples_to_fft_buffer(
	unsigned long n, const float re[], const float im[],
	std::vector<complx> & fftbuf)
{
	const auto s = fftbuf.size();
	assert(n<=s);
	for (unsigned long i=0; i<n; ++i)
		fftbuf[i] = complx(re[i],im[i]);
	for (unsigned long i=n; i<s; ++i)
		fftbuf[i] = complx(0,0);
}

plugin_state::plugin_state(
	unsigned ir_length, float gain,
	const float ir_mid[], const float ir_sid[])
: ir_length(ir_length)
, fftsize(std::max(1024u,nextpow2((ir_length*7)/4)))
{
	for (unsigned k=0; k<num_ports; ++k)
		port_data_locations[k] = 0;
	run_adding_gain = 1;

	// allocate FFT buffers...
	transfer.resize(fftsize,complx(0,0));
	input.resize(fftsize,complx(0,0));
	output.resize(fftsize,complx(0,0));

	// allocate save buffer...
	save.reserve(fftsize);

	// FFT impulse response
	copy_samples_to_fft_buffer(ir_length,ir_mid,ir_sid,transfer);
	for (unsigned k=0; k<ir_length; ++k)
		transfer[k] *= gain;
	fft(transfer);
	cross_talk = true;
}

/// convolves in with tf into out
/// by multiplication in the spectral domain
void conv_complx(
	std::vector<complx> const& in,
	std::vector<complx> const& tf,
	std::vector<complx> & out)
{
	const auto fftsize = in.size();
	assert(in.size()==tf.size());
	assert(tf.size()==out.size());
	for (unsigned i=0; i<fftsize; ++i) {
		out[i] = in[i] * tf[i];
	}
}

/// convolves the real part of in with tf into out
/// by multiplication in the spectral domain
void conv_real(
	std::vector<complx> const& in,
	std::vector<complx> const& tf,
	std::vector<complx> & out)
{
	const auto fftsize = in.size();
	assert(in.size()==tf.size());
	assert(tf.size()==out.size());
	for (unsigned i=0, j=0; i<fftsize; ++i) {
		complx x = (in[i] + conj(in[j])) * real(0.5);
		out[i] =      x  * tf[i];
		out[j] = conj(x) * tf[j];
		j = (j-1) % fftsize;
	}
}

// negative i times
inline complx nitimes(complx const& x)
{
	return complx(x.imag(),-x.real());
}

// negative i times
void nitimes_inplace(std::vector<complx> & io)
{
	const auto fftsize = io.size();
	for (unsigned i=0; i<fftsize; ++i) {
		auto& x = io[i];
		x = nitimes(x);
	}
}

void plugin_state::run(unsigned long SampleCount, bool add)
{
	LADSPA_Data* const* const data = this->port_data_locations;
	const unsigned long max_chunk = fftsize-ir_length+1;
	unsigned long offset = 0;
	while (SampleCount>0) {
		const float* const inL = data[0]+offset; // left  input port
		const float* const inR = data[1]+offset; // right input port
		float* const outL = data[2]+offset; // left  output port
		float* const outR = data[3]+offset; // right output port
		const unsigned long chunk = SampleCount<max_chunk ? SampleCount : max_chunk;
		const unsigned long cnv_chunk = chunk+ir_length-1; // size of convolved chunk
		if (save.size()<cnv_chunk)
			save.resize(cnv_chunk,complx(0,0));

		copy_samples_to_fft_buffer(chunk,inL,inR,input);
		fft(input);
		if (!cross_talk) {
			// convolve left and right at the same time with ir_mid...
			// (this is only ok if ir_sid is all zero)
			conv_complx(input,transfer,output);
			ifft(output);
			for (unsigned i=0; i<cnv_chunk; ++i)
				save[i] += output[i];
		} else {
			// convolve left channel...
			conv_real(input,transfer,output);
			ifft(output);
			for (unsigned i=0; i<cnv_chunk; ++i)
				save[i] += output[i];

			// convolve right channel...
			nitimes_inplace(input);
			conv_real(input,transfer,output);
			ifft(output);
			for (unsigned i=0; i<cnv_chunk; ++i) {
				const auto& oi = output[i];
				save[i] += complx(oi.imag(),oi.real());
			}
		}

		// store mixed results into output port buffers...
		if (add) { // run_adding
			for (unsigned i=0; i<chunk; ++i) {
				const auto& si = save[i];
				outL[i] += si.real() * run_adding_gain;
				outR[i] += si.imag() * run_adding_gain;
			}
		} else { // run (overwriting)
			for (unsigned i=0; i<chunk; ++i) {
				const auto& si = save[i];
				outL[i] = si.real();
				outR[i] = si.imag();
			}
		}
		save.erase(save.begin(),save.begin()+chunk); // consume samples
		SampleCount -= chunk;
		offset += chunk;
	}
}

inline plugin_state* handle2ps(LADSPA_Handle raw)
{
	return static_cast<plugin_state*>(raw);
}

} // anonymous namespace
} // namespace sg

// C interface and plugin configuration
extern "C" {

using namespace sg;

static LADSPA_Handle instantiate(const LADSPA_Descriptor* Descriptor,
                                 unsigned long            SampleRate)
{
	try {
		switch (Descriptor->UniqueID) {
			case unique_id_hrir512:
				return new plugin_state(
					hrir512_length, hrir512_gain,
					hrir512_mid, hrir512_sid );
			case unique_id_hrir80:
				return new plugin_state(
					hrir80_length, hrir80_gain,
					hrir80_mid, hrir80_sid );
			default:
				return 0;
		}
	} catch (std::exception const& x) {
		return 0; // std::bad_alloc?
	}
}

static void connect_port(LADSPA_Handle Instance,
                         unsigned long Port,
                         LADSPA_Data * DataLocation)
{
	handle2ps(Instance)->connect_port(Port,DataLocation);
}

static void activate(LADSPA_Handle Instance)
{
	handle2ps(Instance)->activate();
}

static void run(LADSPA_Handle Instance,
                unsigned long SampleCount)
{
	handle2ps(Instance)->run(SampleCount,false);
}

static void run_adding(LADSPA_Handle Instance,
                       unsigned long SampleCount)
{
	handle2ps(Instance)->run(SampleCount,true);
}

static void set_run_adding_gain(LADSPA_Handle Instance,
                                LADSPA_Data   Gain)
{
	handle2ps(Instance)->set_run_adding_gain(Gain);
}

static void deactivate(LADSPA_Handle Instance)
{
	handle2ps(Instance)->deactivate();
}

static void cleanup(LADSPA_Handle Instance)
{
	delete handle2ps(Instance);
}

static const LADSPA_PortDescriptor pdescr[] = {
	LADSPA_PORT_AUDIO | LADSPA_PORT_INPUT,
	LADSPA_PORT_AUDIO | LADSPA_PORT_INPUT,
	LADSPA_PORT_AUDIO | LADSPA_PORT_OUTPUT,
	LADSPA_PORT_AUDIO | LADSPA_PORT_OUTPUT
};

static const LADSPA_PortRangeHint prhints[] = {
	{ 0, 0, 0 }, // HintDescriptor, LowerBound, UpperBound
	{ 0, 0, 0 }, // HintDescriptor, LowerBound, UpperBound
	{ 0, 0, 0 }, // HintDescriptor, LowerBound, UpperBound
	{ 0, 0, 0 }  // HintDescriptor, LowerBound, UpperBound
};

static const char* const pnames[] = {
	"in-left",
	"in-right",
	"out-left",
	"out-right",
	0
};

static const LADSPA_Descriptor ldescr[] = {
	{
		unique_id_hrir512, // UniqueID
		"hrir512", // label of the filter
		LADSPA_PROPERTY_HARD_RT_CAPABLE, // Properties
		"512-point HRIR stereo filter", // Name
		"sg", // Maker
		"(C) 2013 Sebastian Gesemann", // Copyright
		num_ports, // PortCount
		pdescr, // PortDescriptors
		pnames, // PortNames
		prhints, // PortRangeHints
		0, // ImplementationData
		// function pointers:
		instantiate,
		connect_port,
		activate,
		run,
		run_adding,
		set_run_adding_gain,
		deactivate,
		cleanup
	},{
		unique_id_hrir80, // UniqueID
		"hrir80", // label of the filter
		LADSPA_PROPERTY_HARD_RT_CAPABLE, // Properties
		"80-point HRIR stereo filter", // Name
		"sg", // Maker
		"(C) 2013 Sebastian Gesemann", // Copyright
		num_ports, // PortCount
		pdescr, // PortDescriptors
		pnames, // PortNames
		prhints, // PortRangeHints
		0, // ImplementationData
		// function pointers:
		instantiate,
		connect_port,
		activate,
		run,
		run_adding,
		set_run_adding_gain,
		deactivate,
		cleanup
	}
};

const LADSPA_Descriptor* ladspa_descriptor(unsigned long Index)
{
	const unsigned long Num = sizeof(ldescr)/sizeof(ldescr[0]);
	return (Index<Num) ? ldescr+Index : 0;
}

} // extern "C"

